﻿// Copyright (c) 2012, Alexander Endris
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// * Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
// 
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
// IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#region Usings

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using BinaryReader = NamelessOne.Imaging.IO.BinaryReader;

#endregion

namespace NamelessOne.Imaging.Tiff
{
  /// <summary>
  ///   An image in Tiff Format
  /// </summary>
  internal class TiffImage : Image
  {
    #region Public / Methods

    /// <summary>
    ///   Führt anwendungsspezifische Aufgaben durch, die mit der Freigabe, der Zurückgabe oder dem Zurücksetzen von nicht verwalteten Ressourcen zusammenhängen.
    /// </summary>
    /// <filterpriority>2</filterpriority>
    public override void Dispose()
    {
      reader.Close();
      reader.Dispose();
    }

    /// <summary>
    ///   Gets the color values of the specified pixel
    /// </summary>
    /// <param name = "x">X-Coordinate of the pixel</param>
    /// <param name = "y">Y-Coordinate of the pixel</param>
    /// <returns>Color value of the specified pixel</returns>
    public override Color16 GetPixel(uint x, uint y)
    {
      Strip strip = null;
      uint maxY = 0;

      foreach (var str in strips)
      {
        maxY += str.Rows;
        if (y < maxY)
        {
          maxY -= str.Rows;
          strip = str;
          break;
        }
      }

      if (strip == null)
        throw new ArgumentNullException();

      uint helperY = y - maxY;
      uint bytePosition = (uint) (x*bytesPerPixel + helperY*width*bytesPerPixel);

      byte[] bytes;
      switch (compression)
      {
        case TiffCompression.None:
          reader.Seek(strip.Offset + bytePosition, SeekOrigin.Begin);
          bytes = reader.ReadBytes(bytesPerPixel);
          break;
        case TiffCompression.PackBits:
          byte[] stripBytes = UnpackStrip(strip);
          bytes = new byte[bytesPerPixel];
          Array.Copy(stripBytes, bytePosition, bytes, 0, bytes.Length);
          break;
        default:
          throw new NotSupportedException();
      }

      switch (PixelFormat)
      {
        case PixelFormat.Greyscale8:
          return new Color16(bytes[0], 1);
        case PixelFormat.Greyscale16:
          return new Color16(bytes[0] | bytes[1] << 8, 2);
        case PixelFormat.Rgb16:
          return Color16.FromRGB565(bytes[0], bytes[1]);
        case PixelFormat.Rgb24:
          return new Color16(bytes[0], bytes[1], bytes[2], (byte) 1);
        case PixelFormat.Argb32:
          return new Color16(bytes[0], bytes[1], bytes[2], bytes[3], 1);
        default:
          throw new ArgumentOutOfRangeException();
      }
    }

    /// <summary>
    ///   Reads the next line of pixels and outputs it as a byte array.
    ///   The PixelFormat property has to be taken into account so the values in the byte array can be interpreted correcly
    /// </summary>
    /// <param name = "scanline">A byte array that contains the raw data of the pixels in the scanline. If there is no more scanline the byte array will have a length of zero</param>
    /// <returns>True if there was another scanline, false if there wasn't</returns>
    public override bool ReadNextScanline(out byte[] scanline)
    {
      if (currentScanline >= height)
      {
        scanline = new byte[0];
        return false;
      }

      Strip strip = null;
      uint maxY = 0;

      foreach (var str in strips)
      {
        maxY += str.Rows;
        if (currentScanline < maxY)
        {
          maxY -= str.Rows;
          strip = str;
          break;
        }
      }

      if (strip == null)
        throw new ArgumentNullException();

      uint helperY = (uint) (currentScanline - maxY);

      uint bytePosition = (uint) (helperY*width*bytesPerPixel);

      switch (compression)
      {
        case TiffCompression.None:
          reader.Seek(strip.Offset + bytePosition, SeekOrigin.Begin);
          scanline = reader.ReadBytes(width*bytesPerPixel);
          break;
        case TiffCompression.PackBits:
          byte[] stripBytes = UnpackStrip(strip);
          scanline = new byte[bytesPerPixel*width];
          Array.Copy(stripBytes, bytePosition, scanline, 0, scanline.Length);
          break;
        default:
          throw new NotSupportedException();
      }
      currentScanline++;

      return true;
    }

    /// <summary>
    ///   Resets the internal scanline counter and streams so ReadNextScanline starts at the first scanline in the image.
    /// </summary>
    public override void ResetScanline()
    {
      currentScanline = 0;
    }

    /// <summary>
    ///   Not yet implemented and supported
    /// </summary>
    /// <param name = "x"></param>
    /// <param name = "y"></param>
    /// <param name = "value"></param>
    public override void SetPixel(uint x, uint y, Color16 value)
    {
      throw new NotImplementedException();
    }

    /// <summary>
    ///   Unpacks a PackBit compressed strip
    /// </summary>
    /// <param name = "strip">The strip to be uncompressed</param>
    public byte[] UnpackStrip(Strip strip)
    {
      List<byte> bytes = new List<byte>();
      reader.Seek(strip.Offset, SeekOrigin.Begin);

      // if the strip has already been decompressed use the decompressed bytes.
      if (strips.IndexOf(strip) != lastStripIndex)
      {
        for (int i = 0; i < strip.Rows; i++)
        {
          int wantedBytes = width*bytesPerPixel;

          while (wantedBytes > 0)
          {
            sbyte operation = reader.ReadSByte();

            if (operation >= 0)
            {
              bytes.AddRange(reader.ReadBytes(operation + 1));

              wantedBytes -= operation + 1;
            }
            else if (operation > -128)
            {
              int repetition = -operation + 1;
              byte value = reader.ReadByte();

              bytes.AddRange(Enumerable.Repeat(value, repetition));

              wantedBytes -= repetition;
            }
            else
              continue;
          }
        }
        // Remember the last strip as it might be used again
        lastStripIndex = strips.IndexOf(strip);
        lastStripBytes = bytes.ToArray();
      }

      return lastStripBytes;
    }

    #endregion

    #region Private / Attributes

    private TiffCompression compression;
    private int currentScanline;
    private List<Ifd> ifds;
    private byte[] lastStripBytes;
    private int lastStripIndex = -1;
    private BinaryReader reader;
    private List<Strip> strips;

    #endregion

    #region Private / Constructors

    internal TiffImage(string fileName, bool bigEndian)
    {
      this.fileName = fileName;
      imageFormat = bigEndian ? ImageFormat.TiffBigEndian : ImageFormat.Tiff;
      Initialize(fileName, bigEndian);
    }

    #endregion

    #region Private / Methods

    /// <summary>
    ///   Initializes all fields of this class
    /// </summary>
    /// <param name = "fileName">Full path to the file</param>
    /// <param name = "bigEndian">Indicator if the file is stored ad bigendian</param>
    private void Initialize(string fileName, bool bigEndian)
    {
      using (FileStream stream = new FileStream(fileName, FileMode.Open))
      using (BinaryReader binaryReader = new BinaryReader(stream, bigEndian))
      {
        ushort tiffSignature = binaryReader.ReadUInt16();
        ushort tiffVersion = binaryReader.ReadUInt16();

        uint firstIfdOffset = binaryReader.ReadUInt32();
        binaryReader.Seek(firstIfdOffset, SeekOrigin.Begin);

        ReadIfds(binaryReader, firstIfdOffset);

        foreach (var entry in ifds[0].Entries)
        {
          binaryReader.Seek(entry.ValueOffset, SeekOrigin.Begin);

          switch (entry.TiffTagId)
          {
            case TiffTagId.NewSubfileType:
              break;
            case TiffTagId.ImageWidth:
              if (entry.DataType == TiffDataType.Long)
                width = (int) ReadULong(binaryReader);
              else
                width = ReadUShort(binaryReader);
              break;
            case TiffTagId.ImageHeight:
              if (entry.DataType == TiffDataType.Long)
                height = (int) ReadULong(binaryReader);
              else
                height = ReadUShort(binaryReader);
              break;
            case TiffTagId.BitsPerSample:
              bytesPerPixel = ReadBpp(binaryReader, entry.NumValues)/8;
              break;
            case TiffTagId.Compression:
              compression = (TiffCompression) ReadUShort(binaryReader);
              if (compression != TiffCompression.None && compression != TiffCompression.PackBits)
                throw new NotSupportedException();
              break;
            case TiffTagId.PhotometricInterpretation:
              ushort photometricImpression = ReadUShort(binaryReader);
              switch (bytesPerPixel)
              {
                case 1:
                  if (photometricImpression == 1)
                    pixelFormat = PixelFormat.Greyscale8;
                  break;
                case 2:
                  if (photometricImpression == 1)
                    pixelFormat = PixelFormat.Greyscale16;
                  else if (photometricImpression == 2)
                    pixelFormat = PixelFormat.Rgb16;
                  break;
                case 3:
                  if (photometricImpression == 2)
                    pixelFormat = PixelFormat.Rgb24;
                  break;
              }
              break;
            case TiffTagId.StripOffsets:
              ReadStripOffsets(binaryReader, entry.NumValues, entry.DataType);
              break;
            case TiffTagId.SamplesPerPixel:
              break;
            case TiffTagId.RowsPerStrip:
              ReadRowsPerStrip(binaryReader, entry.DataType);
              break;
            case TiffTagId.StripByteCounts:
              ReadStripByteCounts(binaryReader, entry.NumValues, entry.DataType);
              break;
            case TiffTagId.XResolution:
              break;
            case TiffTagId.YResolution:
              break;
            case TiffTagId.PlanarConfiguration:
              break;
            case TiffTagId.ResolutionUnit:
              break;
            case TiffTagId.Software:
              break;
            case TiffTagId.DateTime:
              break;
            case TiffTagId.ColorMap:
              break;
            case TiffTagId.Xmp:
              break;
            case TiffTagId.Photoshop:
              break;
            case TiffTagId.ExifIfd:
              break;
            default:
              break;
          }
        }
      }

      FileStream str = new FileStream(fileName, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
      reader = new BinaryReader(str, bigEndian);
    }

    /// <summary>
    ///   Determines if the Value/Offset part of an entry is either a value or an offset
    /// </summary>
    /// <param name = "dataType">Datatype stored</param>
    /// <param name = "numValues">Count of values stored</param>
    /// <returns>True if it is an Offset, False if the Value is stored there directly</returns>
    private bool IsOffset(TiffDataType dataType, uint numValues)
    {
      switch (dataType)
      {
          // DataType is 8-Bit
        case TiffDataType.SByte:
        case TiffDataType.Byte:
        case TiffDataType.Undefined:
        case TiffDataType.ASCII:
          return numValues > 4;
          // DataType is 16-Bit
        case TiffDataType.SShort:
        case TiffDataType.Short:
          return numValues > 2;
          // DataType is 32-Bit
        case TiffDataType.SLong:
        case TiffDataType.Long:
        case TiffDataType.Float:
          return numValues > 1;
          // DataType is 64-Bit
        case TiffDataType.SRational:
        case TiffDataType.Rational:
        case TiffDataType.Double:
          return false;
        default:
          throw new ArgumentOutOfRangeException("dataType");
      }
    }

    /// <summary>
    ///   Read bits per pixel
    /// </summary>
    /// <returns>Bits per pixel</returns>
    private int ReadBpp(BinaryReader reader, uint numValues)
    {
      int result = 0;

      for (int i = 0; i < numValues; i++)
        result += ReadUShort(reader);

      return result;
    }

    /// <summary>
    ///   Reads all IFDs in the file and its entries
    /// </summary>
    private void ReadIfds(BinaryReader reader, uint firstIfdOffset)
    {
      ifds = new List<Ifd>();
      uint nextOffset = firstIfdOffset;

      while (nextOffset != 0)
      {
        Ifd current = new Ifd();
        reader.Seek(nextOffset, SeekOrigin.Begin);
        int numEntries = reader.ReadUInt16();

        for (int i = 0; i < numEntries; i++)
        {
          IfdEntry entry = new IfdEntry();
          entry.TiffTagId = (TiffTagId) reader.ReadUInt16();
          entry.DataType = (TiffDataType) reader.ReadUInt16();
          entry.NumValues = reader.ReadUInt32();

          if (IsOffset(entry.DataType, entry.NumValues))
            entry.ValueOffset = reader.ReadUInt32();
          else
          {
            entry.ValueOffset = (uint) reader.Position;
            reader.ReadUInt32();
          }

          current.Entries.Add(entry);
        }

        ifds.Add(current);
        nextOffset = reader.ReadUInt32();
      }
    }

    /// <summary>
    ///   Reads the number of rows per strip
    /// </summary>
    private void ReadRowsPerStrip(BinaryReader reader, TiffDataType dataType)
    {
      uint rows;
      uint rowsTotal = 0;

      rows = dataType == TiffDataType.Long ? ReadULong(reader) : ReadUShort(reader);

      foreach (var strip in strips)
      {
        if (rowsTotal + rows >= height)
          strip.Rows = (uint) height - rowsTotal;
        else
          strip.Rows = rows;

        rowsTotal += rows;
      }
    }

    /// <summary>
    ///   Reads the count of bytes in each strip
    /// </summary>
    private void ReadStripByteCounts(BinaryReader reader, uint count, TiffDataType dataType)
    {
      for (int i = 0; i < count; i++)
        strips[i].ByteCount = dataType == TiffDataType.Long ? ReadULong(reader) : ReadUShort(reader);
    }

    /// <summary>
    ///   Reads the offsets of each strip
    /// </summary>
    private void ReadStripOffsets(BinaryReader reader, uint count, TiffDataType dataType)
    {
      strips = new List<Strip>((int) count);

      for (int i = 0; i < count; i++)
      {
        Strip strip = new Strip();

        strip.Offset = dataType == TiffDataType.Long ? ReadULong(reader) : ReadUShort(reader);

        strips.Add(strip);
      }
    }

    private uint ReadULong(BinaryReader reader)
    {
      return reader.ReadUInt32();
    }

    private ushort ReadUShort(BinaryReader reader)
    {
      return reader.ReadUInt16();
    }

    #endregion

    /// <summary>
    ///   Strip that contains the raw pixel data
    /// </summary>
    public class Strip
    {
      #region Public / Attributes

      public uint ByteCount;
      public uint Offset;
      public uint Rows;

      #endregion
    }
  }
}